package service

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"inventory/api/inventory"
	"inventory/internal/constant"
	"inventory/pkg/orm"
	"inventory/pkg/util"
	"strconv"
	"strings"
	"time"

	"inventory/internal/model"
	"inventory/internal/pkg/generator"
	"inventory/pkg/ecode"
)

// GetPurchaseList 获取采购单列表
func (s *Service) GetPurchaseList(ctx context.Context, req *inventory.GetPurchaseListRequest) (resp *inventory.GetPurchaseListResponse, err error) {
	var endDate, startDate time.Time
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if startDate, endDate, err = verifyStartDateAndEndDate(req.StartDate, req.EndDate); err != nil {
		return
	}
	if _, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.VisitPurchasingOrder); err != nil {
		return
	}
	var (
		total          int64
		purchaseOrders []*model.PurchaseOrder
	)
	if total, purchaseOrders, err = s.dao.FindPurchaseOrders(ctx, sgId, req.Id, startDate,
		endDate, int(req.PageNo), int(req.PageSize), req.QueryOrderNumber); err != nil {
		return
	}

	views := make([]*inventory.PurchaseOrderView, len(purchaseOrders))
	for i, o := range purchaseOrders {
		views[i] = &inventory.PurchaseOrderView{
			OrderNumber: o.OrderNumber,
			CreateTime:  o.CreateTime.Format(util.DefaultLayout),
			DetailCount: o.DetailCount,
			Direction:   uint32(o.Direction),
			Memo:        o.Memo,
			Total:       o.Total,
		}
	}

	resp = &inventory.GetPurchaseListResponse{Orders: views, Total: uint32(total)}
	return
}

// GetPurchaseInfo 获取采购单详情
func (s *Service) GetPurchaseInfo(ctx context.Context, req *inventory.CommonOrderRequest) (resp *inventory.GetPurchaseInfoResponse, err error) {
	var (
		warehouse     *model.Warehouse
		purchaseOrder *model.PurchaseOrder
	)
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.VisitPurchasingOrder); err != nil {
		return
	}
	if purchaseOrder, err = s.dao.GetPurchaseOrder(ctx, sgId, req.OrderNumber); err != nil {
		if errors.Cause(err) == gorm.ErrRecordNotFound {
			err = constant.ErrOrderNotExist
		}
		return
	}
	var purchaseDetails []*model.PurchaseDetail
	if purchaseDetails, err = s.dao.FindPurchaseOrderDetails(ctx, purchaseOrder.ID); err != nil {
		return
	}
	storageKeyMap := make(util.StringBoolMap)
	for _, d := range purchaseDetails {
		storageKeyMap[d.StorageKey] = true
	}
	var storages []*model.Storage
	if storages, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, warehouse.ID, storageKeyMap.GetKeys()); err != nil {
		return
	}
	storageMap := make(map[string]*model.Storage)
	for _, s := range storages {
		storageMap[s.StorageKey] = s
	}
	views := make([]*inventory.PurchaseInfoView, len(purchaseDetails))
	for i, d := range purchaseDetails {
		r := model.PurchaseSnapShotRemark{}
		if err = json.Unmarshal(d.Remark, &r); err != nil {
			return
		}
		storage, ok := storageMap[d.StorageKey]
		if !ok {
			storage = &model.Storage{}
		}
		views[i] = &inventory.PurchaseInfoView{
			StorageType:      inventory.StorageType(d.StorageType),
			StorageKey:       d.StorageKey,
			InitQuantity:     d.Quantity,
			RefundedQuantity: d.RefundedQuantity,
			CurrentQuantity:  storage.Quantity,
			FreezeQuantity:   storage.FreezeQuantity,
			Remark:           d.Remark,
			InitAmount:       int64(float64(d.Price) * d.Quantity / r.PurchaseRatio),
			RefundedAmount:   int64(float64(d.Price) * d.RefundedQuantity / r.PurchaseRatio),
		}

	}
	resp = &inventory.GetPurchaseInfoResponse{
		OrderNumber:   purchaseOrder.OrderNumber,
		Direction:     uint32(purchaseOrder.Direction),
		WarehouseName: warehouse.Name,
		Creator:       s.downstream.GetEmployeeName(ctx, sgId, purchaseOrder.Creator),
		DetailCount:   purchaseOrder.DetailCount,
		Memo:          purchaseOrder.Memo,
		Total:         purchaseOrder.Total,
		Data:          views,
	}
	return

}

// AddPurchaseInfo 新增采购单
func (s *Service) AddPurchaseInfo(ctx context.Context, req *inventory.AddPurchaseInfoRequest) (resp *inventory.CommonOrderReply, err error) {
	var (
		warehouse                      *model.Warehouse
		objectKeys                     []string
		boms                           []*model.BomConfig
		objectRemarks, materialRemarks map[string]*model.FullRemark
		storageArray                   []*model.Storage
		priceMap                       map[string]uint64
	)
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if len(req.Details) == 0 {
		err = ecode.BadRequest("明细数量不允许为0")
		return
	}
	storageKeyMap := make(util.StringBoolMap)
	materialIDMap := make(util.UInt64BoolMap)
	// group ids
	for _, d := range req.Details {
		if d.Quantity <= 0 {
			err = ecode.BadRequest("采购数量必须大于0")
			return
		}
		if _, ok := storageKeyMap[d.StorageKey]; ok {
			err = ecode.BadRequest("明细对象不允许重复")
			return
		}
		storageKeyMap[d.StorageKey] = true
		switch d.StorageType {
		case inventory.StorageType_MATERIAL:
			var materialID int
			if materialID, err = strconv.Atoi(strings.Split(d.StorageKey, "-")[1]); err != nil {
				err = ecode.BadRequest("物料Storage_key格式错误")
				return
			}
			materialIDMap[uint64(materialID)] = true
		default:
			objectKeys = append(objectKeys, d.StorageKey)
		}
	}
	// build remarks
	if len(objectKeys) != 0 {
		if boms, err = s.dao.FindBomByStorageKeys(ctx, objectKeys); err != nil {
			return
		}
		if objectRemarks, err = s.downstream.BuildObjectRemarks(ctx, sgId, objectKeys); err != nil {
			return
		}
		if len(objectRemarks) != len(objectKeys) {
			err = ecode.BadRequest("请求中存在无效的商品SKU")
			return
		}
		for _, o := range objectRemarks {
			if req.PStoreId != 0 && o.PStoreID != 0 && req.PStoreId != o.PStoreID {
				err = ecode.BadRequest("当前所选商品不可用")
				return
			}
		}
		if len(boms) != 0 {
			objNameMap := make(util.StringBoolMap)
			for _, bom := range boms {
				objNameMap[objectRemarks[bom.StorageKey].Name] = true
			}
			err = ecode.AppendMessage(constant.ErrForbiddenAction,
				fmt.Sprintf("所选商品(%s)已配置bom，不能进行盘点", strings.Join(objNameMap.GetKeys(), ",")))
			return
		}
	}
	if len(materialIDMap) != 0 {
		if materialRemarks, err = s.dao.BuildMaterialsRemarks(ctx, sgId, materialIDMap.GetKeys()); err != nil {
			return
		}
		if len(materialRemarks) != len(materialIDMap) {
			err = ecode.BadRequest("请求中存在无效的物料ID")
			return
		}
		for _, o := range materialRemarks {
			if req.PStoreId != 0 && o.PStoreID != 0 && req.PStoreId != o.PStoreID {
				err = ecode.BadRequest("当前所选产品不可用")
				return
			}
		}
	}
	resp = &inventory.CommonOrderReply{}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.CreatePurchasingOrder); err != nil {
			return
		}
		now := time.Now()
		if storageArray, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, req.Id, storageKeyMap.GetKeys()); err != nil {
			return
		}
		storageMap := make(model.StorageMap)
		storageMap.InitStorageMap(storageArray)
		if resp.OrderNumber, err = generator.GenerateOrderNumber(now, constant.OrderTypePurchase, empId); err != nil {
			return
		}
		total := 0
		for _, d := range req.Details {
			if d.StorageType == inventory.StorageType_MATERIAL {
				// 需要换算单位为采购单位的数量 * 采购单价
				total += int(d.Quantity * float64(d.Price) / materialRemarks[d.StorageKey].PurchaseRatio)
			} else {
				total += int(d.Quantity * float64(d.Price))
			}
		}
		purchaseOrder := model.PurchaseOrder{
			ID:          0,
			OrderNumber: resp.OrderNumber,
			WarehouseID: req.Id,
			SgID:        sgId,
			Status:      constant.PurchaseOrderDone,
			Direction:   constant.DirectionIn,
			DetailCount: uint32(len(req.Details)),
			Memo:        req.Memo,
			Total:       int64(total),
			Creator:     empId,
			CreateTime:  now,
		}
		if err = s.dao.Create(ctx, &purchaseOrder); err != nil {
			return err
		}
		// create details
		if priceMap, err = s.dao.GetLastMonthStoragePriceByStorageKeys(ctx, warehouse.ID, storageKeyMap.GetKeys(),
			now, materialRemarks, constant.UnitTypePurchase); err != nil {
			return
		}
		for _, d := range req.Details {
			var (
				prodPointer, materialPointer *uint64
				remark                       *model.FullRemark
				remarkJson                   json.RawMessage
			)
			switch d.StorageType {
			case inventory.StorageType_MATERIAL:
				remark = materialRemarks[d.StorageKey]
				tmpID, _ := strconv.Atoi(strings.Split(d.StorageKey, "-")[1])
				materialID := uint64(tmpID)
				materialPointer = &materialID
			default:
				remark = objectRemarks[d.StorageKey]
				if d.StorageType == inventory.StorageType_PROD {
					tmpID, _ := strconv.Atoi(strings.Split(d.StorageKey, "-")[0])
					prodID := uint64(tmpID)
					prodPointer = &prodID
				}
			}
			if remarkJson, err = json.Marshal(model.PurchaseSnapShotRemark{
				SnapShotRemark: remark.SnapShotRemark,
				PurchaseUnit:   remark.PurchaseUnit,
				PurchaseRatio:  remark.PurchaseRatio,
			}); err != nil {
				return
			}
			lastPrice, _ := priceMap[d.StorageKey]
			if err = s.dao.Create(ctx, &model.PurchaseDetail{
				OrderID:     purchaseOrder.ID,
				WarehouseID: purchaseOrder.WarehouseID,
				Direction:   constant.DirectionIn,
				StorageType: uint8(d.StorageType),
				StorageKey:  d.StorageKey,
				Remark:      orm.JSON(remarkJson),
				Price:       d.Price,
				LastPrice:   lastPrice,
				Quantity:    d.Quantity,
				CreateTime:  now,
			}); err != nil {
				return
			}
			if purchaseOrder.Status == constant.PurchaseOrderDone {
				if remarkJson, err = json.Marshal(model.StorageRealTimeRemark{
					Remark:        remark.Remark,
					Type:          remark.TypeID,
					StorageUnit:   remark.StorageUnit,
					StorageRatio:  remark.StorageRatio,
					PurchaseUnit:  remark.PurchaseUnit,
					PurchaseRatio: remark.PurchaseRatio,
				}); err != nil {
					return
				}
				if err = s.dao.CreateOrIncreaseStorage(ctx, &model.Storage{
					WarehouseID:    warehouse.ID,
					StorageKey:     d.StorageKey,
					StorageType:    uint8(d.StorageType),
					Quantity:       d.Quantity,
					FreezeQuantity: 0,
					StorageMode:    warehouse.StorageMode,
					ProdID:         prodPointer,
					MaterialID:     materialPointer,
					Remark:         orm.JSON(remarkJson),
					UpdateTime:     now,
				}); err != nil {
					return
				}
				if err = s.dao.Create(ctx, &model.StatsDaily{
					RecordDate:    now,
					ActionType:    constant.ActionPurchaseIn,
					WarehouseID:   warehouse.ID,
					StorageKey:    d.StorageKey,
					StorageType:   uint8(d.StorageType),
					StorageName:   remark.Name,
					Quantity:      d.Quantity,
					Price:         int64(d.Price),
					OperationUnit: remark.PurchaseUnit,
					Amount:        int64(float64(d.Price) * d.Quantity / remark.PurchaseRatio),
				}); err != nil {
					return
				}
			}
		}
		return
	})
	return
}

// AddPurchaseRefund 新增采购退货单
func (s *Service) AddPurchaseRefund(ctx context.Context, req *inventory.AddPurchaseRefundRequest) (resp *inventory.CommonOrderReply, err error) {
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	resp = &inventory.CommonOrderReply{}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var (
			purchaseDetails []*model.PurchaseDetail
			warehouse       *model.Warehouse
			storages        []*model.Storage
			priceMap        map[string]uint64
		)
		if len(req.Details) == 0 {
			err = ecode.BadRequest("明细数量不允许为0")
			return
		}
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.CreatePurchasingOrder); err != nil {
			return
		}
		purchaseOrder := model.PurchaseOrder{}
		if err = s.dao.Lock(ctx, model.PurchaseOrder{
			SgID:        sgId,
			OrderNumber: req.OrderNumber,
		}, &purchaseOrder); err != nil || purchaseOrder.WarehouseID != warehouse.ID {
			if err == nil || errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrOrderNotExist
			}
			return
		}
		if purchaseDetails, err = s.dao.FindPurchaseOrderDetails(ctx, purchaseOrder.ID); err != nil {
			return
		}
		storageKeyMap := make(util.StringBoolMap)
		detailMap := make(map[int]*model.PurchaseDetail)
		// 明细前置校验
		for _, d := range req.Details {
			if d.Quantity <= 0 {
				err = ecode.BadRequest("退款数量必须大于0")
				return
			}
			index := int(d.Index)
			if index+1 > len(purchaseDetails) || purchaseDetails[d.Index].StorageKey != d.StorageKey || inventory.StorageType(purchaseDetails[d.Index].StorageType) != d.StorageType {
				err = ecode.BadRequest("找不到对应明细")
				return
			}
			if d.Quantity > purchaseDetails[d.Index].Quantity-purchaseDetails[d.Index].RefundedQuantity {
				err = ecode.BadRequest("可退数量不足")
				return
			}
			if _, ok := storageKeyMap[d.StorageKey]; ok {
				err = ecode.BadRequest("明细重复")
				return
			}
			detailMap[index] = purchaseDetails[d.Index]
			storageKeyMap[d.StorageKey] = true
		}
		if storages, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, purchaseOrder.WarehouseID, storageKeyMap.GetKeys()); err != nil {
			return
		}
		storageMap := make(model.StorageMap)
		storageMap.InitStorageMap(storages)
		for _, d := range req.Details {
			if storage, ok := storageMap[d.StorageKey]; ok {
				if storage.Quantity-storage.FreezeQuantity < d.Quantity {
					// 可退库存不足
					err = formatStorageUnavailableError(storage)
					return
				}
			}
		}
		now := time.Now()
		if resp.OrderNumber, err = generator.GenerateOrderNumber(now, constant.OrderTypePurchase, empId); err != nil {
			return
		}
		total := 0
		materialRemarks := make(map[string]*model.FullRemark)
		for _, d := range req.Details {
			if d.StorageType == inventory.StorageType_MATERIAL {
				// 需要换算单位为采购单位的数量 * 采购单价
				detail := detailMap[int(d.Index)]
				detailRemark := &model.PurchaseSnapShotRemark{}
				if err = json.Unmarshal(detail.Remark, &detailRemark); err != nil {
					return
				}
				materialRemarks[d.StorageKey] = &model.FullRemark{
					SnapShotRemark: detailRemark.SnapShotRemark,
					PurchaseUnit:   detailRemark.PurchaseUnit,
					PurchaseRatio:  detailRemark.PurchaseRatio,
				}
				total += int(d.Quantity * float64(d.Price) / detailRemark.PurchaseRatio)
			} else {
				total += int(d.Quantity * float64(d.Price))
			}
		}
		purchaseRefundOrder := model.PurchaseOrder{
			ID:             0,
			OrderNumber:    resp.OrderNumber,
			WarehouseID:    purchaseOrder.WarehouseID,
			SgID:           sgId,
			Status:         constant.PurchaseOrderDone,
			Direction:      constant.DirectionOut,
			DetailCount:    uint32(len(req.Details)),
			Memo:           req.Memo,
			RefOrderNumber: purchaseOrder.OrderNumber,
			Total:          -int64(total),
			Creator:        empId,
			CreateTime:     now,
		}
		if err = s.dao.Create(ctx, &purchaseRefundOrder); err != nil {
			return err
		}
		affectedMaterialIDMap := make(util.UInt64BoolMap)
		if priceMap, err = s.dao.GetLastMonthStoragePriceByStorageKeys(ctx, warehouse.ID, storageKeyMap.GetKeys(),
			now, materialRemarks, constant.UnitTypePurchase); err != nil {
			return
		}
		for _, d := range req.Details {
			detail := detailMap[int(d.Index)]
			detailRemark := &model.PurchaseSnapShotRemark{}
			if err = json.Unmarshal(detail.Remark, &detailRemark); err != nil {
				return
			}
			// 创建退货明细
			lastPrice, _ := priceMap[d.StorageKey]
			if err = s.dao.Create(ctx, &model.PurchaseDetail{
				OrderID:     purchaseRefundOrder.ID,
				WarehouseID: purchaseOrder.WarehouseID,
				Direction:   constant.DirectionOut,
				StorageType: uint8(d.StorageType),
				StorageKey:  d.StorageKey,
				Remark:      detail.Remark,
				Price:       d.Price,
				LastPrice:   lastPrice,
				Quantity:    -d.Quantity,
				CreateTime:  purchaseRefundOrder.CreateTime,
			}); err != nil {
				return
			}
			// 更新原始明细
			if err = s.dao.Update(ctx, &model.PurchaseDetail{
				ID: detail.ID,
			}, map[string]interface{}{
				"refunded_quantity": gorm.Expr("inv_purchase_detail.refunded_quantity + ?", d.Quantity),
			}); err != nil {
				return
			}
			// 更新日结记录
			if err = s.dao.Create(ctx, &model.StatsDaily{
				RecordDate:    now,
				ActionType:    constant.ActionPurchaseOut,
				WarehouseID:   warehouse.ID,
				StorageKey:    d.StorageKey,
				StorageType:   uint8(d.StorageType),
				StorageName:   detailRemark.Name,
				Quantity:      -d.Quantity,
				Price:         int64(d.Price),
				OperationUnit: detailRemark.PurchaseUnit,
				Amount:        int64(float64(d.Price) * -d.Quantity / detailRemark.PurchaseRatio),
			}); err != nil {
				return
			}
			// 更新库存
			var affected bool
			if affected, err = s.dao.DecreaseStorage(ctx, purchaseOrder.WarehouseID, d.StorageKey, d.Quantity,
				purchaseRefundOrder.CreateTime); err != nil {
				return
			}
			if affected {
				// 只有存量库存变更才触发库存通知
				if d.StorageType == inventory.StorageType_MATERIAL {
					materialID, _ := strconv.Atoi(d.StorageKey[2:])
					affectedMaterialIDMap[uint64(materialID)] = true
				}
			} else {
				delete(storageKeyMap, d.StorageKey)
			}
		}
		return
	})
	return
}
